import 'dart:typed_data';

class SignatureMethod {
  static Map<String, dynamic> splitSignature(dynamic signature) {
    final result = {
      'r': '0x',
      's': '0x',
      '_vs': '0x',
      'recoveryParam': 0,
      'v': 0,
      'yParityAndS': '0x',
      'compact': '0x',
    };

    List<int> bytes = arrayify(signature);
    // Get the r, s and v
    if (bytes.length == 64) {
      // EIP-2098; pull the v from the top bit of s and clear it
      result['v'] = 27 + (bytes[32] >> 7);
      bytes[32] &= 0x7f;
      result['r'] = hexlify(bytes.sublist(0, 32));
      result['s'] = hexlify(bytes.sublist(32, 64));
    } else if (bytes.length == 65) {
      result['r'] = hexlify(bytes.sublist(0, 32));
      result['s'] = hexlify(bytes.sublist(32, 64));
      result['v'] = bytes[64];
    } else {
      throw ArgumentError('invalid signature string');
    }

    // Allow a recid to be used as the v
    int v = int.parse(result['v'].toString());
    if (v < 27) {
      if (result['v'] == 0 || result['v'] == 1) {
        v += 27;
      } else {
        throw ArgumentError('signature invalid v byte');
      }
    }

    // Compute recoveryParam from v
    result['recoveryParam'] = 1 - (v % 2);
    result['v'] = v;
    // Compute _vs from recoveryParam and s
    if (result['recoveryParam'] == 1) {
      bytes[32] |= 0x80;
    }
    result['_vs'] = hexlify(bytes.sublist(32, 64));

    return result;
  }

  static bool isBytesLike(dynamic value) {
    return (isHexString(value) && (value.length % 2 == 0)) || isBytes(value);
  }

  static bool isHexString(dynamic value, [int? length]) {
    if (!(value is String) || !RegExp(r'^0x[0-9A-Fa-f]*$').hasMatch(value)) {
      return false;
    }
    if (length != null && value.length != 2 + 2 * length) {
      return false;
    }
    return true;
  }

  static bool isBytes(dynamic value) {
    if (value == null) {
      return false;
    }
    if (value is List<int> || value is Uint8List) {
      return true;
    }
    if (value is String) {
      return false;
    }
    if (!(value is Iterable) || value.length < 0) {
      return false;
    }
    for (dynamic v in value) {
      if (!(v is int) || v < 0 || v >= 256) {
        return false;
      }
    }
    return true;
  }

  static List<int> _encode(dynamic object) {
    if (object is List) {
      List<int> payload = [];
      object.forEach((child) {
        payload.addAll(_encode(child));
      });
      if (payload.length <= 55) {
        payload.insert(0, 0xc0 + payload.length);
        return payload;
      }
      List<int> length = arrayifyInteger(payload.length);
      length.insert(0, 0xf7 + length.length);
      return length + payload;
    }
    if (!(isBytesLike(object))) {
      throw ArgumentError('RLP object must be a list of integers');
    }
    List<int> data = List.from(arrayify(object));
    if (data.length == 1 && data[0] <= 0x7f) {
      return data;
    } else if (data.length <= 55) {
      data.insert(0, 0x80 + data.length);
      return data;
    }
    List<int> length = arrayifyInteger(data.length);
    length.insert(0, 0xb7 + length.length);
    return length + data;
  }

  static encode(object) {
    return hexlify(_encode(object));
  }

  static bool isHexable(value) {
    return value is String && int.tryParse(value, radix: 16) != null;
  }

  static String hexlify(dynamic value, {bool allowMissingPrefix = false, String hexPad = ''}) {
    var HexCharacters = '0123456789abcdef';
    if (value is int) {
      String hex = '';
      while (value != 0) {
        hex = HexCharacters[value & 0xf] + hex;
        value = value ~/ 16;
      }
      if (hex.isNotEmpty) {
        if (hex.length % 2 != 0) {
          hex = '0' + hex;
        }
        return '0x' + hex;
      }
      return '0x00';
    }

    if (value is BigInt) {
      String hex = value.toRadixString(16);
      if (hex.length % 2 != 0) {
        hex = '0' + hex;
      }
      return '0x' + hex;
    }

    if (allowMissingPrefix && value is String && !value.startsWith('0x')) {
      value = '0x' + value;
    }

    if (isHexable(value)) {
      return value.toHexString();
    }

    if (isHexString(value)) {
      if (value.length % 2 != 0) {
        if (hexPad == 'left') {
          value = '0x0' + value.substring(2);
        } else if (hexPad == 'right') {
          value += '0';
        } else {
          throw ArgumentError('hex data is odd-length: $value');
        }
      }
      return value.toLowerCase();
    }

    if (value is List<int> || value is Uint8List) {
      String result = '0x';
      for (int i = 0; i < value.length; i++) {
        int v = value[i];
        result += HexCharacters[(v & 0xf0) >> 4] + HexCharacters[v & 0x0f];
      }
      return result;
    }

    throw ArgumentError('invalid hexlify value: $value');
  }

  static List<int> arrayifyInteger(int value) {
    List<int> result = [];
    while (value != 0) {
      result.insert(0, value & 0xff);
      value >>= 8;
    }
    return result;
  }

  static List<int> stripZeros(List<int> value) {
    List<int> result = value;
    if (result.length == 0) {
      return result;
    }
    // Find the first non-zero entry
    int start = 0;
    while (start < result.length && result[start] == 0) {
      start++;
    }
    // If we started with zeros, strip them
    if (start > 0) {
      result = result.sublist(start);
    }
    return result;
  }

  static bool isArrayish(dynamic value) {
    if (value == null || int.tryParse(value.length.toString()) != value.length || value is String) {
      return false;
    }

    for (var i = 0; i < value.length; i++) {
      var v = value[i];
      if (v < 0 || v >= 256 || int.tryParse(v.toString()) != v) {
        return false;
      }
    }

    return true;
  }

  static Uint8List arrayify2(dynamic value, {bool allowMissingPrefix = false, String hexPad = ''}) {
    if (value is int) {
      final result = <int>[];
      while (value > 0) {
        result.insert(0, value & 0xff);
        value = value ~/ 256;
      }
      if (result.isEmpty) {
        result.add(0);
      }
      return Uint8List.fromList(result);
    }
    if (allowMissingPrefix && value is String && !value.startsWith('0x')) {
      value = '0x$value';
    }
    if (isHexable(value)) {
      value = value;
    }
    if (value is String && isHexString(value)) {
      String hex = value.substring(2);
      if (hex.length % 2 != 0) {
        if (hexPad == 'left') {
          hex = '0$hex';
        } else if (hexPad == 'right') {
          hex += '0';
        } else {
          throw ArgumentError('hex data is odd-length');
        }
      }
      final result = <int>[];
      for (int i = 0; i < hex.length; i += 2) {
        result.add(int.parse(hex.substring(i, i + 2), radix: 16));
      }
      return Uint8List.fromList(result);
    }
    if (value is List<int>) {
      return Uint8List.fromList(value);
    }
    throw ArgumentError('invalid arrayify value');
  }

  static Uint8List arrayify(dynamic value) {
    if (value == null) {
      throw ArgumentError.value(value, 'value', 'cannot convert null value to array');
    }

    if (value is String) {
      var match = RegExp(r'^(0x)?[0-9a-fA-F]*$').firstMatch(value);
      if (match == null) {
        throw ArgumentError.value(value, 'value', 'invalid hexadecimal string');
      }
      if (match.group(1) != '0x') {
        throw ArgumentError.value(value, 'value', 'hex string must have 0x prefix');
      }
      value = value.substring(2);

      if (value.length % 2 != 0) {
        value = '0' + value;
      }

      var result = <int>[];
      for (var i = 0; i < value.length; i += 2) {
        result.add(int.parse(value.substring(i, i + 2), radix: 16));
      }

      return Uint8List.fromList(result);
    }

    throw ArgumentError.value(value, 'value', 'invalid arrayify value');
  }
}
